/*
 *  Created at 2011-01-30 10:25 GMT-03:00
 *  Author: Victor Williams Stafusa da Silva
 *  JSmartUnit
 */
package org.jsmartunit.statement;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.jsmartunit.model.ClassStructure;

import org.jsmartunit.model.InstanceFactory;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;

/**
 * @author Victor Williams Stafusa da Silva
 */
public class RunBeforesAfters {

    private RunBeforesAfters() {
    }

    private static interface MethodRunner {
        public void run(Method aroundMethod) throws Throwable;
    }

    private static class ExplosiveRunner implements MethodRunner {

        private final Object instance;
        private final Object[] args;

        public ExplosiveRunner() {
            this.instance = null;
            this.args = new Object[0];
        }

        public ExplosiveRunner(Object instance, Object... args) {
            this.instance = instance;
            this.args = args;
        }

        @Override
        public void run(Method aroundMethod) throws Throwable {
            ClassStructure.invokeExplosively(instance, aroundMethod, args);
        }
    }

    private static class AroundInstanceRunner implements MethodRunner {

        private final InstanceFactory<?> instanceFactory;
        private final Method target;

        public AroundInstanceRunner(InstanceFactory<?> instanceFactory, Method target) {
            this.instanceFactory = instanceFactory;
            this.target = target;
        }

        @Override
        public void run(Method aroundMethod) throws Throwable {
            Object instance = instanceFactory.getInstance();
            if (instance == null) throw new IllegalStateException(instanceFactory.getClass().getName());
            ClassStructure.invokeExplosively(instance, aroundMethod, getParametersBeforeAfters(aroundMethod));
        }

        private Object[] getParametersBeforeAfters(Method around) throws InitializationError {
            Class<?>[] params = around.getParameterTypes();
            if (params.length == 0) return new Object[0];
            if (params.length == 1 && params[0] == Method.class && target != null) return new Object[] {target};
            throw new InitializationError("Does not know how to call the method " + around + ".");
        }
    }

    private static class RunBefores extends Statement {

        private final Statement next;
        private final MethodRunner runner;
        private final List<Method> befores;

        RunBefores(Statement next, List<Method> befores, MethodRunner runner) {
            this.next = next;
            this.befores = befores;
            this.runner = runner;
        }

        @Override
        public void evaluate() throws Throwable {
            for (Method before : befores) {
                runner.run(before);
            }
            next.evaluate();
        }
    }

    private static class RunAfters extends Statement {

        private final Statement next;
        private final MethodRunner runner;
        private final List<Method> afters;

        RunAfters(Statement next, List<Method> afters, MethodRunner runner) {
            this.next = next;
            this.afters = afters;
            this.runner = runner;
        }

        @Override
        public void evaluate() throws Throwable {
            List<Throwable> errors = new ArrayList<Throwable>(10);

            try {
                next.evaluate();
            } catch (Throwable e) {
                errors.add(e);
            } finally {
                for (Method after : afters) {
                    try {
                        runner.run(after);
                    } catch (Throwable e) {
                        errors.add(e);
                    }
                }
            }
            MultipleFailureException.assertEmpty(errors);
        }
    }

    private static Statement beforeMethod(Statement next, ClassStructure struct, InstanceFactory<?> instanceFactory, Method method) throws InitializationError {
        List<Method> befores = struct.getBeforeMethods();
        validateBeforesAfters(method, befores);
        return new RunBefores(next, befores, new AroundInstanceRunner(instanceFactory, method));
    }

    private static Statement afterMethod(Statement next, ClassStructure struct, InstanceFactory<?> instanceFactory, Method method) throws InitializationError {
        List<Method> afters = struct.getAfterMethods();
        validateBeforesAfters(method, afters);
        return new RunAfters(next, afters, new AroundInstanceRunner(instanceFactory, method));
    }

    public static Statement aroundMethod(Statement next, InstanceFactory<?> instanceFactory, Method method) throws InitializationError {
        InstanceFactory<?> cached = InstanceFactory.Factories.cache(instanceFactory);
        ClassStructure struct = ClassStructure.getStructure(cached.getTargetClass());
        return afterMethod(beforeMethod(next, struct, cached, method), struct, cached, method);
    }

    private static Statement beforeClass(Statement next, ClassStructure struct) throws InitializationError {
        List<Method> befores = struct.getBeforeClassMethods();
        validateBeforesAftersClass(befores);
        return new RunBefores(next, befores, new ExplosiveRunner());
    }

    private static Statement afterClass(Statement next, ClassStructure struct) throws InitializationError {
        List<Method> afters = struct.getAfterClassMethods();
        validateBeforesAftersClass(afters);
        return new RunAfters(next, afters, new ExplosiveRunner());
    }

    public static Statement aroundClass(Statement next, Class<?> targetClass) throws InitializationError {
        ClassStructure struct = ClassStructure.getStructure(targetClass);
        return afterClass(beforeClass(next, struct), struct);
    }

    private static void validateBeforesAfters(Method invoked, List<Method> arounds) throws InitializationError {
        List<Throwable> errors = new LinkedList<Throwable>();
        for (Method around : arounds) {
            Class<?>[] params = around.getParameterTypes();
            new FrameworkMethod(around).validatePublicVoid(false, errors);
            if (params.length == 0) continue;
            if (params.length == 1 && params[0] == Method.class && invoked != null) continue;
            errors.add(new InitializationError("Does not know how to call the method " + around + "."));
        }
        if (!errors.isEmpty()) throw new InitializationError(errors);
    }

    private static void validateBeforesAftersClass(List<Method> arounds) throws InitializationError {
        List<Throwable> errors = new LinkedList<Throwable>();
        for (Method around : arounds) {
            new FrameworkMethod(around).validatePublicVoidNoArg(true, errors);
        }
        if (!errors.isEmpty()) throw new InitializationError(errors);
    }
}
